﻿using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using ShevaDeferredRenderer.Lights;
using ShevaDeferredRenderer.Shadows;
using ShevaEngine.Core.Attributes;
using ShevaEngine.Core.Foundation;
using ShevaEngine.Core.Interfaces;
using ShevaEngine.Core.Modules.Graphics;
using ShevaEngine.Core.Modules.Graphics.Lights;
using ShevaEngine.Core.Modules.Materials;
using ShevaDeferredRenderer.PostProcess;
using System.Collections.Specialized;

namespace ShevaDeferredRenderer
{
    /// <summary>
    /// Deferred renderer.
    /// </summary>
    public class DeferredRenderer : IRenderer
    {
        /// <summary>Graphics viewport.</summary>
        public GraphicsViewport Viewport;

        /// <summary>Additive blending state. The resulting color will be added to current render target color.</summary>
        private BlendState AdditiveBlendingState;
        /// <summary>Light Pre Pass Map.</summary>
        public RenderTarget2D LightMap { get; private set; }
        /// <summary>Scene texture in HDR format (before tone mapping).</summary>
        public RenderTarget2D SceneHDRTexture { get; private set; }        
        /// <summary>Scene Final Texture (post processed).</summary>
        public RenderTarget2D SceneFinalTexture { get; private set; }
        /// <summary>Post processing.</summary>
        public PostProcessing PostProcessing;
        /// <summary>G Buffer.</summary>
        internal GBuffer GBuffer;
        /// <summary>Last draw call.</summary>
        private RenderingPackage LastRenderingPair;
        /// <summary>Initial draw call.</summary>
        private RenderingPackage InitialRenderingPair;
        /// <summary>Ambient light.</summary>
        public AmbientLight AmbientLight;
        /// <summary>Draw mutex.</summary>
        public Mutex DrawMutex;
        /// <summary>InstancesVBO.</summary>
        public DynamicVertexBuffer InstancesVertexBuffer;
        /// <summary>Matrices.</summary>
        private List<Matrix> Matrices;
        /// <summary>DrawCall key.</summary>
        private DrawCallKey DrawCallKey;
        /// <summary>DrawCall key.</summary>
        private DrawCallKey LastDrawCallKey;

        /// <summary>Lighting.</summary>
        [EditableAttribute(true), SettingsControlTypeAttribute(SettingsControlType.CheckBox)]
        public Boolean LightingEnabled;
        /// <summary>Lighting.</summary>
        [EditableAttribute(true), SettingsControlTypeAttribute(SettingsControlType.ColorPicker)]
        public Color ClearColor;
        /// <summary>Shadow maps.</summary>
        public Dictionary<Light, ShadowMap> ShadowMaps;
      
        
        /// <summary>
        /// Deferred renderer constructor.
        /// </summary>
        /// <param name="camera"></param>
        /// <param name="scene"></param>
        public DeferredRenderer(GraphicsViewport viewport)
        {
            this.Viewport = viewport;
            this.Viewport.ViewportResize += new OnViewportResizeCallback(Viewport_ViewportResize);

            this.DrawMutex = new Mutex();

            this.CreateBuffers();

            this.InitialRenderingPair = new RenderingPackage();
            this.InitialRenderingPair.DrawCall = DrawCallManager.Instance.CreateIndexedPrimitivesDrawCall(
                PrimitiveType.LineList, 0, 0, 0, 0, 0);
            this.InitialRenderingPair.DrawCallKey = new DrawCallKey(UInt16.MaxValue, UInt16.MaxValue, UInt16.MaxValue, UInt16.MaxValue);
            this.Matrices = new List<Matrix>();

            this.LightingEnabled = true;
            this.ClearColor = Color.Black;

            this.ShadowMaps = new Dictionary<Light, ShadowMap>();

            foreach (Light light in this.Viewport.Scene.Lights)
            {
                switch (light.ShadowMapType)
                {
                    case ShadowMapType.DualParaboloid:
                        this.ShadowMaps.Add(light, new DualParaboloidSM());
                        break;                    
                }
            }

            this.InstancesVertexBuffer = new DynamicVertexBuffer(ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice,
                        InstanceVertexDeclaration.BlendWeight, 128, BufferUsage.WriteOnly);

            this.Viewport.Scene.Lights.CollectionChanged += new NotifyCollectionChangedEventHandler(this.LightAdded);
            this.Viewport.Scene.Lights.CollectionChanged -= new NotifyCollectionChangedEventHandler(this.LightRemoved);

            this.PostProcessing = new PostProcessing();
        }       

        /// <summary>
        /// Method creates buffers.
        /// </summary>
        /// <param name="viewport"></param>
        private void CreateBuffers()
        {
            this.DrawMutex.WaitOne();

            this.GBuffer = new GBuffer(this.Viewport);
            this.AmbientLight = new AmbientLight(this.Viewport);

            this.LightMap = DeferredRenderer.CreateRenderTarget(this.Viewport, RenderTargetSizeType.FullScreen, SurfaceFormat.Color, DepthFormat.None, RenderTargetAntialiasingType.NoAntialiasing);

            this.AdditiveBlendingState = new BlendState
            {
                AlphaBlendFunction = BlendFunction.Add,
                AlphaDestinationBlend = Blend.One,
                AlphaSourceBlend = Blend.One,
                ColorBlendFunction = BlendFunction.Add,
                ColorDestinationBlend = Blend.One,
                ColorSourceBlend = Blend.One,
            };

            this.SceneHDRTexture = DeferredRenderer.CreateRenderTarget(this.Viewport, RenderTargetSizeType.FullScreen, SurfaceFormat.HdrBlendable, DepthFormat.Depth24, RenderTargetAntialiasingType.NoAntialiasing);
            this.SceneFinalTexture = DeferredRenderer.CreateRenderTarget(this.Viewport, RenderTargetSizeType.FullScreen, SurfaceFormat.Color, DepthFormat.None, RenderTargetAntialiasingType.NoAntialiasing);

            this.DrawMutex.ReleaseMutex();
        }

        /// <summary>
        /// Viewport resized event.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="viewport"></param>
        void Viewport_ViewportResize(object sender, GraphicsViewport viewport)
        {
            this.CreateBuffers();
        }      

         /// <summary>
        /// Creates a render target for render to textures.
        /// </summary>
        /// <param name="sizeType">Render target size</param>
        /// <param name="highPrecisionSingleChannelFormat">Uses a rgba 8 bit per channel format or a 32 bit single channel format</param>
        /// <param name="hasDepthBuffer">has depth buffer?</param>
        /// <param name="antialiasingType">Multi sampling type: System value or no antialiasing.</param>
        internal static RenderTarget2D CreateRenderTarget(GraphicsViewport viewport, RenderTargetSizeType sizeType, bool highPrecisionSingleChannelFormat = false, bool hasDepthBuffer = true, RenderTargetAntialiasingType antialiasingType = RenderTargetAntialiasingType.NoAntialiasing)
        {
            System.Drawing.Size size = CalculateSize(viewport, sizeType);            

            return new RenderTarget2D(ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice, 
                size.Width, size.Height, 
                false, 
                highPrecisionSingleChannelFormat ? SurfaceFormat.Single : SurfaceFormat.Color,
                hasDepthBuffer ? DepthFormat.Depth24 : DepthFormat.None,
                antialiasingType == RenderTargetAntialiasingType.System ? ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.PresentationParameters.MultiSampleCount : 0, 
                RenderTargetUsage.PlatformContents);
        }

        /// <summary>
        /// Creates a render target for render to textures.
        /// </summary>
        /// <param name="sizeType">Render target size</param>
        /// <param name="surfaceFormat">Surface format</param>
        /// <param name="hasDepthBuffer">Has depth buffer?</param>
        /// <param name="antialiasingType">Multi sampling type: System value or no antialiasing.</param>
        internal static RenderTarget2D CreateRenderTarget(GraphicsViewport viewport, RenderTargetSizeType sizeType, SurfaceFormat surfaceFormat, bool hasDepthBuffer = true, RenderTargetAntialiasingType antialiasingType = RenderTargetAntialiasingType.NoAntialiasing)
        {
            System.Drawing.Size size = CalculateSize(viewport, sizeType);                       

            return new RenderTarget2D(ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice,
                size.Width, size.Height,
                false,
                surfaceFormat,
                hasDepthBuffer ? DepthFormat.Depth24 : DepthFormat.None,
                antialiasingType == RenderTargetAntialiasingType.System ? ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.PresentationParameters.MultiSampleCount : 0,
                RenderTargetUsage.DiscardContents);          
        }

        /// <summary>
        /// Creates a render target for render to textures.
        /// </summary>
        /// <param name="sizeType">Render target size</param>
        /// <param name="surfaceFormat">Surface format</param>
        /// <param name="depthFormat">Depth Format</param>
        /// <param name="antialiasingType">Multi sampling type: System value or no antialiasing.</param>
        internal static RenderTarget2D CreateRenderTarget(GraphicsViewport viewport, RenderTargetSizeType sizeType, SurfaceFormat surfaceFormat, DepthFormat depthFormat, RenderTargetAntialiasingType antialiasingType = RenderTargetAntialiasingType.NoAntialiasing)
        {
            System.Drawing.Size size = CalculateSize(viewport, sizeType);
                        
            return new RenderTarget2D(ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice,
                size.Width, size.Height,
                false,
                surfaceFormat,
                depthFormat,
                antialiasingType == RenderTargetAntialiasingType.System ? ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.PresentationParameters.MultiSampleCount : 0,
                RenderTargetUsage.DiscardContents);    
        }       

        /// <summary>
        /// Calculate size from size type.
        /// </summary>
        internal static System.Drawing.Size CalculateSize(GraphicsViewport viewport, RenderTargetSizeType sizeType)
        {
            int width;
            int height;
            switch (sizeType)
            {
                case RenderTargetSizeType.FullScreen:
                    width = viewport.PositionSize.Width;
                    height = viewport.PositionSize.Height;
                    break;
                case RenderTargetSizeType.HalfScreen:
                    width = viewport.PositionSize.Width / 2;
                    height = viewport.PositionSize.Height / 2;
                    break;
                case RenderTargetSizeType.QuarterScreen:
                    width = viewport.PositionSize.Width / 4;
                    height = viewport.PositionSize.Height / 4;
                    break;
                case RenderTargetSizeType.Square256X256:
                    width = 256;
                    height = 256;
                    break;
                case RenderTargetSizeType.Square512X512:
                    width = 512;
                    height = 512;
                    break;
                case RenderTargetSizeType.Square1024X1024:
                    width = 1024;
                    height = 1024;
                    break;
                default:
                    throw new Exception("Render Target error. Size type doesn't exist (probably a bug).");
            }
            return new System.Drawing.Size(width, height);
        } 

        /// <summary>
        /// Update.
        /// </summary>
        public static void Update()
        {
            
        }

        
        /// <summary>
        /// Fill the G buffer.
        /// For the moment only depth and normals.
        /// Also in this method the downsampled version of the g buffer is created.
        /// </summary>
        private void FillGBuffer(RenderingPipeline pipeline)
        {
            this.GBuffer.GenerateGBuffer(pipeline);            
        }

        /// <summary>
        /// Fill Light Map. This is the light pre pass.
        /// </summary>
        private void FillLightMap()
        {
            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.SetRenderTarget(this.LightMap);

            if (!this.LightingEnabled)
            {
                ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.Clear(new Color(1.0f, 1.0f, 1.0f, 1.0f));

                ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.SetRenderTarget(null);

                return;
            }

            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.Clear(new Color(0.0f, 0.0f, 0.0f, 0.0f));

            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.BlendState = this.AdditiveBlendingState;
            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.DepthStencilState = DepthStencilState.None;

            this.AmbientLight.Render(GBuffer.NormalTexture, this.LightMap);

            foreach (Light light in this.Viewport.Scene.Lights)
                if (light.Enabled)
                    switch (light.ShadowMapType)
                    {
                        case ShadowMapType.None:
                            LightPrePass.Instance.RenderNoneSM(light, this.Viewport.Camera,
                                this.GBuffer.DepthTexture, this.GBuffer.NormalTexture, this.GBuffer.MotionVectorsSpecularPowerTexture, this.LightMap);
                            break;
                        case ShadowMapType.DualParaboloid:
                            LightPrePass.Instance.RenderDualParaboloidSM(light, this.ShadowMaps[light] as DualParaboloidSM, this.Viewport.Camera,
                                this.GBuffer.DepthTexture, this.GBuffer.NormalTexture, this.GBuffer.MotionVectorsSpecularPowerTexture, this.LightMap);
                            break;
                        default:
                            break;
                    }

            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.SetRenderTarget(null);
        }
      
        

        /// <summary>
        /// Fill Scene Map (material pass)
        /// </summary>
        private void FillSceneMap(RenderingPipeline pipeline)
        {
            RasterizerState rState = new RasterizerState();
            rState.FillMode = FillMode.WireFrame;
            rState.CullMode = CullMode.CullCounterClockwiseFace;
            //ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.RasterizerState = rState;

            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.BlendState = BlendState.Opaque;
            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.RasterizerState = RasterizerState.CullNone;

            DepthStencilState dState = new DepthStencilState();
            dState.DepthBufferEnable = true;
            dState.DepthBufferFunction = CompareFunction.LessEqual;
            dState.DepthBufferWriteEnable = true;

            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.DepthStencilState = dState;
            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.SetRenderTarget(this.SceneHDRTexture);
            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.Clear(this.ClearColor);

            if (pipeline.ActualCount == 0)
                return;

            Effect actualEffect = null;

            this.LastRenderingPair = this.InitialRenderingPair;
            this.Matrices.Clear();


            this.Matrices.Add(pipeline.DrawCalls[0].Matrix);

            this.DrawCallKey = pipeline.DrawCalls[0].DrawCallKey;

            GraphicsMaterial material = MaterialManager.Instance[this.DrawCallKey.MaterialID].GraphicsProfile;

            actualEffect = material.Effect;

            actualEffect.Parameters["View"].SetValue(this.Viewport.Camera.ViewMatrix);
            actualEffect.Parameters["Proj"].SetValue(this.Viewport.Camera.ProjectionMatrix);

            actualEffect.Parameters["LightMap"].SetValue(this.LightMap);
            actualEffect.Parameters["CameraPosition"].SetValue(this.Viewport.Camera.Position);

            material.SetDefaultGraphicsMaterialParameters();
            this.LastDrawCallKey.MaterialParamsID = DrawCallKey.None;

            actualEffect.CurrentTechnique.Passes[0].Apply();

            if (this.DrawCallKey.TextureState != DrawCallKey.None)
            {
                TextureState textureState = TextureManager.Instance[this.DrawCallKey.TextureState];

                foreach (KeyValuePair<string, Texture> texture in textureState)
                    actualEffect.Parameters[texture.Key].SetValue(texture.Value);
            }

            if (this.DrawCallKey.MaterialParamsID != DrawCallKey.None)
                MaterialManager.Instance[this.DrawCallKey.MaterialID].GraphicsProfile.SetGraphicsMaterialParameters(this.DrawCallKey.MaterialParamsID);

            if (this.DrawCallKey.BuffersID != DrawCallKey.None)
                GraphicsBuffersManager.Instance.SetBuffers(this.DrawCallKey.BuffersID);

            this.LastDrawCallKey = this.DrawCallKey;
            this.LastRenderingPair = pipeline.DrawCalls[0];

            for (int i = 1; i < pipeline.ActualCount; i++)
            {
                if (pipeline.DrawCalls[i].DrawCallKey != this.LastRenderingPair.DrawCallKey ||
                    pipeline.DrawCalls[i].DrawCall != this.LastRenderingPair.DrawCall)
                {
                    DrawCall.RenderDrawCalls(actualEffect, this.LastRenderingPair.DrawCall, this.Matrices.ToArray());

                    this.Matrices.Clear();
                    this.Matrices.Add(pipeline.DrawCalls[i].Matrix);

                    this.DrawCallKey = pipeline.DrawCalls[i].DrawCallKey;

                    if (this.DrawCallKey.MaterialID != this.LastDrawCallKey.MaterialID)
                    {
                        material = MaterialManager.Instance[this.DrawCallKey.MaterialID].GraphicsProfile;

                        actualEffect = material.Effect;

                        actualEffect.Parameters["View"].SetValue(this.Viewport.Camera.ViewMatrix);
                        actualEffect.Parameters["Proj"].SetValue(this.Viewport.Camera.ProjectionMatrix);

                        actualEffect.Parameters["LightMap"].SetValue(this.LightMap);
                        actualEffect.Parameters["CameraPosition"].SetValue(this.Viewport.Camera.Position);

                        material.SetDefaultGraphicsMaterialParameters();
                        this.LastDrawCallKey.MaterialParamsID = DrawCallKey.None;

                        actualEffect.CurrentTechnique.Passes[0].Apply();                        
                    }


                    if (this.DrawCallKey.TextureState != DrawCallKey.None &&
                        this.DrawCallKey.TextureState != this.LastDrawCallKey.TextureState)
                    {
                        TextureState textureState = TextureManager.Instance[this.DrawCallKey.TextureState];

                        foreach (KeyValuePair<string, Texture> texture in textureState)
                            actualEffect.Parameters[texture.Key].SetValue(texture.Value);                        
                    }
                    
                    if (this.DrawCallKey.MaterialParamsID != DrawCallKey.None &&
                        this.DrawCallKey.MaterialParamsID != this.LastDrawCallKey.MaterialParamsID)
                    {
                        MaterialManager.Instance[this.DrawCallKey.MaterialID].GraphicsProfile.SetGraphicsMaterialParameters(this.DrawCallKey.MaterialParamsID);
                    }


                    if (this.DrawCallKey.BuffersID != DrawCallKey.None &&
                        this.DrawCallKey.BuffersID != this.LastDrawCallKey.BuffersID)
                        GraphicsBuffersManager.Instance.SetBuffers(this.DrawCallKey.BuffersID);

                    this.LastDrawCallKey = this.DrawCallKey;
                    this.LastRenderingPair = pipeline.DrawCalls[i];
                }
                else
                    this.Matrices.Add(pipeline.DrawCalls[i].Matrix);
            }

            DrawCall.RenderDrawCalls(actualEffect, this.LastRenderingPair.DrawCall, this.Matrices.ToArray());

            if (this.Viewport.Gui != null)
                this.Viewport.Gui.Draw();

            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.SetRenderTarget(null);
            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.RasterizerState = RasterizerState.CullNone;            
        }
        
        /// <summary>
        /// Render to frame buffer and post process.
        /// </summary>
        private void PostProcess()
        {
            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.SetRenderTarget(this.SceneFinalTexture);
            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.Clear(Color.Black);

            this.PostProcessing.Render(this.SceneHDRTexture);

            ShevaEngine.Core.ShevaEngine.Instance.GraphicsDevice.SetRenderTarget(null);            
        } 
        
        /// <summary>
        /// Render the added objects using the deferred lighting system.
        /// </summary>
        public Texture2D Draw()
        {
            this.DrawMutex.WaitOne();
            
            RenderingPipeline pipeline = 
                this.Viewport.Scene.GetRenderingPipeline(this.Viewport.Camera);

            this.FillGBuffer(pipeline);
            
            this.FillLightMap();

            this.FillSceneMap(pipeline);
            
            this.PostProcess();
            
            this.DrawMutex.ReleaseMutex();

            return this.SceneFinalTexture;            
        }

        /// <summary>
        /// Method creates SM.
        /// </summary>
        public void CreateShadowMaps()
        {
            lock (this.ShadowMaps)
            {
                foreach (KeyValuePair<Light, ShadowMap> shadowMap in this.ShadowMaps)
                    shadowMap.Value.CreateShadowMap(shadowMap.Key, this.Viewport.Scene);
            }
        }

        /// <summary>
        /// Event light added.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void LightAdded(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
                foreach (Light light in e.NewItems)
                    switch (light.ShadowMapType)
                    {
                        case ShadowMapType.DualParaboloid:
                            lock (this.ShadowMaps)
                                this.ShadowMaps.Add(light, new DualParaboloidSM());
                            break;
                    }
        }

        /// <summary>
        /// Event light removed.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void LightRemoved(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Remove)
                foreach (Light light in e.OldItems)
                    lock (this.ShadowMaps)
                    {
                        if (this.ShadowMaps.ContainsKey(light))
                            this.ShadowMaps.Remove(light);
                    }
        }
    }

    /// <summary>
    /// Posible size types for creating a RenderTarget texture.
    /// </summary>
    public enum RenderTargetSizeType
    {
        /// <summary>Uses the full screen size.</summary>
        FullScreen,
        /// <summary>Uses half the full screen size, e.g. 800x600 becomes 400x300</summary>
        HalfScreen,
        /// <summary>Uses a quarter of the full screen size, e.g. 800x600 becomes 200x150</summary>
        QuarterScreen,
        /// <summary>256 x 256 pixels. Good for shadows.</summary>
        Square256X256,
        /// <summary>512 x 512 pixels. Good for shadows.</summary>
        Square512X512,
        /// <summary>1024 x 1024 pixels. Good for shadows.</summary>
        Square1024X1024,
    }

    /// <summary>
    /// Antialiasing Type.
    /// </summary>
    public enum RenderTargetAntialiasingType
    {
        /// <summary>System value.</summary>
        System,
        /// <summary>No antialiasing.</summary>
        NoAntialiasing
    }
}
